//
//  LocationManager.swift
//  LocationDemo
//
//  Created by HzS on 2023/2/1.
//

import UIKit
import CoreLocation
import CoreMotion

/// 整体思路是app运行时，1、获取一次定位，将获取的定位当作圆心，并设置半径形成地理围栏。2、离开这个围栏时触发回调，然后开始步骤1

protocol LocationManagerDelegate: AnyObject {
    func locationManagerShouldStartLocationServices(_ manager: LocationManager) -> Bool
    func locationManager(_ manager: LocationManager, didUpdateLocation location: CLLocation, address: String, userActivity: LocationManager.UserActivityType)
}

class LocationManager: NSObject {
    
    static let didChangeAuthorizationNotification = Notification.Name("LocationManagerDidChangeAuthorizationNotification")
    
    static let shared: LocationManager = LocationManager()
    weak var delegate: LocationManagerDelegate?
    
    let database = Database()
    var currentAuthorizationStatus: CLAuthorizationStatus {
        if #available(iOS 14.0, *) {
            return locationManager.authorizationStatus
        } else {
            return CLLocationManager.authorizationStatus()
        }
    }
    
    private var region: CLCircularRegion?
    @UserDefaultCustom(key: "kLastOriginLoccation", defaultValue: nil)
    private var lastOriginLoccation: LocationModel? // 最近一个火星坐标点
    private lazy var locationManager: CLLocationManager = {
        let manager = CLLocationManager()
        // 获取定位的精度要求（对startMonitoringSignificantLocationChanges无效）
        manager.desiredAccuracy = kCLLocationAccuracyBest
        // 持续定位时用户移动多远距离，才进行定位更新（对startMonitoringSignificantLocationChanges无效）
        manager.distanceFilter = 50
        // 允许后台运行
        manager.allowsBackgroundLocationUpdates = true
        // 不允许系统自己暂停
        manager.pausesLocationUpdatesAutomatically = false
        manager.delegate = self
        return manager
    }()
    
    private let activityManager: CMMotionActivityManager = {
        let manager = CMMotionActivityManager()
        // 第一次时申请权限
        if CMMotionActivityManager.isActivityAvailable() && CMMotionActivityManager.authorizationStatus() == .notDetermined {
            manager.startActivityUpdates(to: .main) { _ in
            }
        }
        return manager
    }()
    
    private(set) var lastAuthorizationStatus: CLAuthorizationStatus = .notDetermined
    private var isUpdatingLocation: Bool = false
    
    private var waitingTaskIdentifier: UIBackgroundTaskIdentifier?
    private var waitingTimer: Timer?
    private var waitingLocations: [CLLocation] = []
    
    private var regionTimer: Timer?
    private var isUpdateFromRegionTimer: Bool = false
    
    let timeFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss SSS"
        return formatter
    }()
    
    override init() {
        super.init()
        
        lastAuthorizationStatus = currentAuthorizationStatus
        
        NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { _ in
            MGFileLogger("app willEnterForeground")
        }
        
        NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in
            MGFileLogger("app willResignActive")
            
            if UIApplication.shared.applicationState != .background { return }
            // 创建一个小围栏
            if let lastOriginLoccation = self?.lastOriginLoccation {
                self?.startMonitoringRegion(center: CLLocationCoordinate2D(latitude: lastOriginLoccation.latitude, longitude: lastOriginLoccation.longitude), radius: 15)
                MGFileLogger("willTerminateNotification：创建一个小围栏")
            } else {
                MGFileLogger("willTerminateNotification：当前没有建围栏")
            }
        }
        
        NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
            MGFileLogger("app didBecomeActive")
            if UIApplication.shared.applicationState != .background { return }
            self?.startLocationServices()
        }
        
        NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { _ in
            MGFileLogger("app didEnterBackground")
        }
        
        NotificationCenter.default.addObserver(forName: UIApplication.willTerminateNotification, object: nil, queue: nil) { [weak self] _ in
            MGFileLogger("app willTerminate")
            // 创建一个小围栏
            if let lastOriginLoccation = self?.lastOriginLoccation {
                self?.startMonitoringRegion(center: CLLocationCoordinate2D(latitude: lastOriginLoccation.latitude, longitude: lastOriginLoccation.longitude), radius: 15)
                MGFileLogger("willTerminateNotification：创建一个小围栏")
            } else {
                MGFileLogger("willTerminateNotification：当前没有建围栏")
            }
        }
    }
    
    func startLocationServices() {
//        if delegate?.locationManagerShouldStartLocationServices(self) != true { return }
        // 开启持续定位
        startUpdatingLocation()
        // 开启重要位置变化定位（可后台唤醒，要移动比较远的距离后才会触发位置更新）
        locationManager.startMonitoringSignificantLocationChanges()
    }
    
    func stopLocationServices() {
        stopUpdatingLocation()
        locationManager.stopMonitoringSignificantLocationChanges()
        stopMonitoringAllRegions()
    }
    
    func checkAndRequestAuthorization() -> Bool {
        if (CLLocationManager.authorizationStatus() == .denied || CLLocationManager.authorizationStatus() == .notDetermined) {
            // 没有获取到权限，再次请求授权
            locationManager.requestWhenInUseAuthorization()
        } else if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
            locationManager.requestAlwaysAuthorization()
        } else if CLLocationManager.authorizationStatus() == .authorizedAlways {
            return true
        }
        return false
    }
    
    private func startUpdatingLocation(_ isForce: Bool = false) {
        MGFileLogger("startUpdatingLocation")
        if isUpdatingLocation && !isForce { return }
        isUpdatingLocation = true
        // 强制发起定位（当前开着的话再调用不会发生任何事，必须关后再打开）
        if isForce { locationManager.stopUpdatingLocation() }
        locationManager.startUpdatingLocation()
    }
    
    private func stopUpdatingLocation() {
        isUpdatingLocation = false
        locationManager.stopUpdatingLocation()
        MGFileLogger("stopUpdatingLocation")
    }
    
    private func startWaitingTimer() {
        // 因为延迟3秒后再处理，防止被系统干掉，申请更多的处理时间
        beginWaitingBackgroundTask()
        
        waitingTimer?.invalidate()
        waitingTimer = Timer(timeInterval: 3, target: PoWeakProxy(target: self), selector: #selector(handleWaitingTimerEvent), userInfo: nil, repeats: false)
        RunLoop.current.add(waitingTimer!, forMode: .common)
//        PoTimer(fireTime: 3, timeInterval: 0, target: self, selector: #selector(handleWaitingTimerEvent), repeats: false)
    }
    
    private func cancelWaitingTimer() {
        waitingTimer?.invalidate()
        waitingTimer = nil
    }
    
    /// 120秒后检查是否已经创建围栏，否则创建一个
    private func startRegionTimer() {
//        regionTimer?.invalidate()
//
//        regionTimer = Timer(timeInterval: 120, target: PoWeakProxy(target: self), selector: #selector(handleRegionTimerEvent), userInfo: nil, repeats: false)
//        RunLoop.current.add(regionTimer!, forMode: .common)
//        isUpdateFromRegionTimer = false
    }
    
    private func cancelRegionTimer() {
//        isUpdateFromRegionTimer = false
//        regionTimer?.invalidate()
//        regionTimer = nil
    }
    
    @objc
    private func handleRegionTimerEvent() {
        isUpdateFromRegionTimer = true
        MGFileLogger("围栏定时器回调：发起一次位置更新")
        startUpdatingLocation(true)
    }
    
    // MARK: - BackgroundTask
    
    private func beginWaitingBackgroundTask() {
        if let waitingTaskIdentifier {
            UIApplication.shared.endBackgroundTask(waitingTaskIdentifier)
        }
        waitingTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
            if let waitingTaskIdentifier = self?.waitingTaskIdentifier {
                UIApplication.shared.endBackgroundTask(waitingTaskIdentifier)
            }
            self?.waitingTaskIdentifier = nil
        })
    }
    
    private func endWaitingBackgroundTask() {
        if let waitingTaskIdentifier {
            UIApplication.shared.endBackgroundTask(waitingTaskIdentifier)
        }
        self.waitingTaskIdentifier = nil
    }
    
    private func logFilterLocation(locations: [CLLocation], findLocation: CLLocation) {
        guard let last = lastOriginLoccation?.toCLLocation else { return }
        for location in locations {
            if findLocation.timestamp != location.timestamp {
                MGFileLogger("过滤的点距离：\(location.distance(from: last)) 精度: \(Int(location.horizontalAccuracy))")
            }
        }
        MGFileLogger("找到的点距离：\(findLocation.distance(from: last)) 精度: \(Int(findLocation.horizontalAccuracy))")
    }
    
    @objc
    private func handleWaitingTimerEvent() {
        MGFileLogger("\(#function)")
        // 清理定时器
        cancelWaitingTimer()
        defer { self.endWaitingBackgroundTask() }
        
        // 从待处理点中找寻最优点
        let location = findBestLocation(in: waitingLocations)
        if let location { logFilterLocation(locations: waitingLocations, findLocation: location) }
        waitingLocations.removeAll()
        
        let isUpdateFromRegionTimer = isUpdateFromRegionTimer
        self.isUpdateFromRegionTimer = false
        guard let location else { return }
        
        // 拿到上次最后的定位
        let lastOriginLoccation = lastOriginLoccation?.toCLLocation
        
        // 是围栏定时器触发的更新
        if isUpdateFromRegionTimer {
            // 清理定时器
            cancelRegionTimer()
            
            // 2分钟后还没移动10米，则认为已到目的地，暂停持续更新，创建围栏
            if shouldFilterLocation(location, lastLocation: lastOriginLoccation) {
                self.stopUpdatingLocation()
                MGFileLogger("开启小围栏")
                startMonitoringRegion(center: location.coordinate, radius: 15)
                return
            }
            // 开启新的围栏创建计时器
            startRegionTimer()
            self.lastOriginLoccation = LocationModel(clLocation: location)
            // 反地理解码后获取到的是地球坐标
            CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (pms, err) -> Void in
                guard let newLocation = pms?.last?.location else {
                    MGFileLogger("反地理解码失败，重新定位")
                    self.startUpdatingLocation(true)
                    return
                }
                self.database.save(location: newLocation, horizontalAccuracy: location.horizontalAccuracy)
                var address: String = ""
                if let placemark = pms?.last {
                    var name  = placemark.name
                    if placemark.subThoroughfare != nil && placemark.thoroughfare != nil{
                        name = "\(placemark.thoroughfare!)\(placemark.subThoroughfare!)"
                    }
                    address = "\(placemark.locality ?? "")\(placemark.subLocality ?? "")\(name ?? "")"
                }
                self.delegate?.locationManager(self, didUpdateLocation: newLocation, address: address, userActivity: .unknown)
                self.logLocation(newLocation, address: address, originLocation: location, lastOriginLocation: lastOriginLoccation)
            })
        } else {
            // 开启新的围栏创建计时器
            startRegionTimer()
            
            if shouldFilterLocation(location, lastLocation: lastOriginLoccation) {
                return
            }
            
            // 检查用户的运动类型
            calculateUserActivityAndAppropriateUpdateDistance(location: location, lastLocation: lastOriginLoccation) { activity, distance in
                MGFileLogger("--- 用户运动状态:\(activity) 建议定位距离:\(Int(distance))")
                self.locationManager.distanceFilter = distance
                if self.shouldFilterLocation(location, by: activity) {
                    // 被过滤了，开始新的定位
                    self.locationManager.distanceFilter = 30
                    return
                }
                self.lastOriginLoccation = LocationModel(clLocation: location)
                // 反地理解码后获取到的是地球坐标
                CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (pms, err) -> Void in
                    guard let newLocation = pms?.last?.location else {
                        MGFileLogger("反地理解码失败，重新定位")
                        self.startUpdatingLocation(true)
                        return
                    }
                    self.database.save(location: newLocation, horizontalAccuracy: location.horizontalAccuracy)
                    var address: String = ""
                    if let placemark = pms?.last {
                        var name  = placemark.name
                        if placemark.subThoroughfare != nil && placemark.thoroughfare != nil{
                            name = "\(placemark.thoroughfare!)\(placemark.subThoroughfare!)"
                        }
                        address = "\(placemark.locality ?? "")\(placemark.subLocality ?? "")\(name ?? "")"
                    }
                    self.delegate?.locationManager(self, didUpdateLocation: newLocation, address: address, userActivity: activity)
                    self.logLocation(newLocation, address: address, originLocation: location, lastOriginLocation: lastOriginLoccation)
                })
            }
        }

    }
    
    private func findBestLocation(in locations: [CLLocation]) -> CLLocation? {
        let locations = locations.filter({ $0.horizontalAccuracy >= 0 })
        if locations.count <= 1 { return locations.first }

        // 筛选出精度小于150的
        let accuracy150 = locations.filter({ $0.horizontalAccuracy < 150 })
        // 如果精度小于150的存在，直接返回最小精度的
        if !accuracy150.isEmpty {
            return accuracy150.min(by: { $0.horizontalAccuracy < $1.horizontalAccuracy })
        }

        // 与上一个点的距离升序排序
        if let lastOriginLoccation = lastOriginLoccation?.toCLLocation {
            let sortedDistance = locations.sorted(by: { $0.distance(from: lastOriginLoccation) < $1.distance(from: lastOriginLoccation) })
            if sortedDistance.count <= 2 { // 取最短
                return sortedDistance.first
            } else { // 取中位数
                return sortedDistance[sortedDistance.count / 2]
            }
        } else { // 没有上一个点作为比较，则找一个精度最好的
            return locations.min(by: { $0.horizontalAccuracy < $1.horizontalAccuracy })
        }
    }
    
    // MARK: - Private methods
    
    private func logLocation(_ location: CLLocation, address: String, originLocation: CLLocation, lastOriginLocation: CLLocation?) {
        var distance: Double = 0
        if let lastOriginLocation {
            distance = originLocation.distance(from: lastOriginLocation)
        }
        MGFileLogger("location: [\(timeFormatter.string(from: originLocation.timestamp)), \(location.coordinate.latitude), \(location.coordinate.longitude), \(Int(originLocation.speed)), \(Int(originLocation.horizontalAccuracy)), \(Int(distance)), \(address)]")
    }
    
    /// 开启地理围栏
    private func startMonitoringRegion(center: CLLocationCoordinate2D, radius: CLLocationDistance) {
        stopMonitoringAllRegions()
        
        region = CLCircularRegion(center: center, radius: radius, identifier: "UpdateCircle")
        region?.notifyOnExit = true
        locationManager.startMonitoring(for: region!)
    }
    
    /// 移除已设置的地理围栏
    private func stopMonitoringAllRegions() {
        locationManager.monitoredRegions.forEach { region in
            locationManager.stopMonitoring(for: region)
        }
        region = nil
    }
    
    /// 根据点来过滤
    private func shouldFilterLocation(_ location: CLLocation, lastLocation: CLLocation?) -> Bool {
        guard let lastOriginLoccation = lastLocation else {
            return false
        }
        
        let distance = location.distance(from: lastOriginLoccation)
        if distance < 10 {
            MGFileLogger("过滤原因：与上一个点距离 \(distance)m")
            return true
        }
        
        let timeInterval = location.timestamp.timeIntervalSince(lastOriginLoccation.timestamp)
        if timeInterval < 2 {
            MGFileLogger("过滤原因：与上一个点时间间隔 \(timeInterval)s")
//            return true
        }
        
        let speed = distance / timeInterval
        if speed > 98 { // 高铁最高时速
            MGFileLogger("过滤原因：速度达到 \(speed)m/s")
//            return true
        }
        return false
    }
    
    /// 根据点和用户行为来过滤
    private func shouldFilterLocation(_ location: CLLocation, by userActivity: UserActivityType) -> Bool {
        switch userActivity {
        case .stationary, .walking:
            if location.horizontalAccuracy > 150 {
                return true
            }
        default:
            break
        }
        return false
    }
}

// MARK: - CLLocationManagerDelegate
extension LocationManager: CLLocationManagerDelegate {
    
    /// 定位更新，获取到的是火星坐标
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        
        waitingLocations.append(location)
        if waitingTimer == nil { // 开启一个2秒后的回调
            startWaitingTimer()
        }
        MGFileLogger("位置更新回调:\(waitingLocations.count)")
    }
    
    /// 成功开启地理围栏
    func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
        MGFileLogger("成功开启地理围栏")
    }
    
    /// 设置地理围栏后，出围栏的回调
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        // 出围栏后我们再获取一次定位，然后建立新的围栏
        MGFileLogger("走出地理围栏")
    }
    
    /// 地理围栏状态改变
    func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        MGFileLogger("地理围栏状态变化：\(state.rawValue)")
        if state == .outside {
            startUpdatingLocation()
        }
    }
    
    /// 地理围栏 监控失败
    func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
        MGFileLogger("++++ monitoringDidFail\(error.localizedDescription)")
        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
            self.startUpdatingLocation()
        })
    }
    
    /// 当获取定位出错时调用
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        startUpdatingLocation()
        MGFileLogger("++++ 定位错误：\(error.localizedDescription)")
    }
    
    /// 此回调暂时用不到，已经关闭了系统自动暂停 pausesLocationUpdatesAutomatically
    func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
        isUpdatingLocation = false
        MGFileLogger("++++ 定位暂停了")
        
        // 没有围栏，则创建一个; 有围栏暂停也没关系
        if locationManager.monitoredRegions.isEmpty {
            // 可以创建一个小围栏
            if let lastOriginLoccation {
                stopUpdatingLocation()
                startMonitoringRegion(center: CLLocationCoordinate2D(latitude: lastOriginLoccation.latitude, longitude: lastOriginLoccation.longitude), radius: 15)
                MGFileLogger("DidPauseLocation：创建一个小围栏")
            } else { // 无法创建一个围栏，则5s后强制定位一次
                DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
                    self.startUpdatingLocation(true)
                })
            }
        }
    }

    func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) {
        isUpdatingLocation = true
        MGFileLogger("++++ 定位恢复了")
    }
    
    /// 定位授权，每次app启动都会回调一次
    @available(iOS 14.0, *)
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        let newStatus = manager.authorizationStatus
        if newStatus != lastAuthorizationStatus {
            NotificationCenter.default.post(name: LocationManager.didChangeAuthorizationNotification, object: nil)
            lastAuthorizationStatus = newStatus
            if checkAndRequestAuthorization() {
                startLocationServices()
            }
        }
    }
    
    /// 定位授权，每次app启动都会回调一次
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        let newStatus = status
        if newStatus != lastAuthorizationStatus {
            NotificationCenter.default.post(name: LocationManager.didChangeAuthorizationNotification, object: nil)
            lastAuthorizationStatus = newStatus
            if checkAndRequestAuthorization() {
                startLocationServices()
            }
        }
    }
    
}

// MARK: - Helper
extension LocationManager {
    
    enum UserActivityType: Int {
        case unknown
        case stationary
        case walking
        case running
        case cycling
        case automotive
        case highSpeed
        
        var toCLActivityType: CLActivityType {
            switch self {
            case .unknown, .stationary, .highSpeed:
                return .other
            case .walking, .running, .cycling:
                return .fitness
            case .automotive:
                return .automotiveNavigation
            }
        }
    }
    
    private func calculateUserActivityAndAppropriateUpdateDistance(location: CLLocation, lastLocation: CLLocation?, completion: @escaping ((UserActivityType, CLLocationDistance) -> Void)) {
        if CMMotionActivityManager.isActivityAvailable() && CMMotionActivityManager.authorizationStatus() == .authorized {
            activityManager.startActivityUpdates(to: .main) { activity in
                var userActivityType: UserActivityType = .unknown
                var distance = 50.0
                if activity?.stationary == true {
                    userActivityType = .stationary
                } else if activity?.walking == true {
                    userActivityType = .walking
                } else if activity?.running == true {
                    userActivityType = .running
                    distance = 60
                } else if activity?.cycling == true {
                    userActivityType = .cycling
                    distance = 80
                } else if activity?.automotive == true {
                    userActivityType = .automotive
                    if let lastLocation = lastLocation {
                        distance = max(self.calculateUpdateDistance(location: location, lastLocation: lastLocation), 150)
                    } else {
                        distance = 150
                    }
                } else {
                    userActivityType = self.calculateUserActivity(location: location, lastLocation: lastLocation)
                    distance = self.calculateUpdateDistance(location: location, lastLocation: lastLocation)
                }
                self.activityManager.stopActivityUpdates()
                completion(userActivityType, distance)
            }
        } else {
            let userActivityType = calculateUserActivity(location: location, lastLocation: lastLocation)
            let distance = calculateUpdateDistance(location: location, lastLocation: lastLocation)
            completion(userActivityType, distance)
        }
    }
    
    /// 根据速度计算用户运动类型
    private func calculateUserActivity(location: CLLocation, lastLocation: CLLocation?) -> UserActivityType {
        guard let lastLocation = lastLocation else { return .unknown }
        let distance = location.distance(from: lastLocation)
        let time = abs(location.timestamp.timeIntervalSince(lastLocation.timestamp))
        let speed = distance / time
        let activity: UserActivityType
        if speed < 0.2 { // 静止
            activity = .stationary
        } else if speed < 2 { // 人正常走路
            activity = .walking
        } else if speed < 3 { // 跑步
            activity = .running
        } else if speed < 5 { // 自行车
            activity = .cycling
        } else if speed < 10 { // 电动车，摩托车，公交
            activity = .automotive
        } else if speed < 40 { // 汽车
            activity = .automotive
        } else {
            activity = .highSpeed // 超高速
        }
        return activity
    }
    
    /// 根据移动速度来计算地理围栏的半径
    private func calculateUpdateDistance(location: CLLocation, lastLocation: CLLocation?) -> CLLocationDistance {
        guard let lastLocation = lastLocation else { return 50 }
        let distance = location.distance(from: lastLocation)
        let time = abs(location.timestamp.timeIntervalSince(lastLocation.timestamp))
        let speed = distance / time // m/s
        let res: CLLocationDistance
        
        if speed < 0.2 { // 静止
            res = 50
        } else if speed < 2 { // 人正常走路
            res = 50
        } else if speed < 3 { // 跑步
            res = 60
        } else if speed < 5 { // 自行车
            res = 80
        } else if speed < 10 { // 电动车，摩托车，公交
            res = 150
        } else if speed < 40 { // 汽车
            res = 150
        } else {
            res = 500 // 超高速
        }
        return res
    }
    
}
